package com.jvxb.manage.livable.service.dbDeploy.impl;

import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.Condition;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.jvxb.common.constants.DbConstants;
import com.jvxb.common.constants.DeployConstants;
import com.jvxb.common.utils.BeanUtil;
import com.jvxb.common.utils.CommonUtil;
import com.jvxb.common.utils.IPUtil;
import com.jvxb.manage.livable.entity.*;
import com.jvxb.manage.livable.service.*;
import com.jvxb.manage.livable.service.dbDeploy.BaseDeployService;
import com.jvxb.manage.livable.vo.ClusterApplyVo;
import com.jvxb.manage.livable.vo.WorkOrderOperateVo;
import com.jvxb.modules.configuration.exception.ServiceException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;

import java.util.*;
import java.util.stream.Collectors;

/**
 * 通用DB部署ServiceImpl，具体部署逻辑在子类实现中
 *
 * @author jvxb
 * @since 2022-01-10
 */
public abstract class BaseDeployServiceImpl implements BaseDeployService {

    @Autowired
    DbDeployInfoService dbDeployInfoService;
    @Autowired
    DbDeployInstanceService deployInstanceService;
    @Autowired
    WorkOrderService workOrderService;
    @Autowired
    AssetsClusterService assetsClusterService;
    @Autowired
    AssetsInstanceService assetsInstanceService;
    @Autowired
    AssetsDbPackageService dbPackageService;
    @Autowired
    AssetsAgentHostService agentHostService;
    @Autowired
    SysDictService sysDictService;

    @Override
    public List<AssetsAgentHost> checkAndGetResource(DbDeployInfo dbDeployInfo, boolean getResource) {
        //1）根据规格确认本次部署的实例数，检查可用的部署机器
        int deployInsNum = getDeployInsNum(dbDeployInfo);
        //条件：同DB类型、未锁定、可部署实例数>0，已部署实例=0（独占）、同环境（此处忽略）、内存/磁盘足够（此处忽略）
        List<AssetsAgentHost> agentHostList = agentHostService.list(Condition.<AssetsAgentHost>create()
                .eq(AssetsAgentHost.DBTYPE, dbDeployInfo.getDbType())
                .eq(AssetsAgentHost.IS_LOCK, AssetsAgentHost.UNLOCK_STATU)
                .eq(AssetsAgentHost.IS_INIT, AssetsAgentHost.INIT_STATU)
                .gt(AssetsAgentHost.SUPPORT_INS_NUM, 0)
                .eq(dbDeployInfo.getPackageName().equalsIgnoreCase(DbConstants.PACKAGE_X), AssetsAgentHost.EXISTS_INS_NUM, 0)
                .orderByAsc(AssetsAgentHost.CREATE_TIME)
        );
        if (agentHostList.size() < deployInsNum) {
            throw new ServiceException(String.format("机器资源不足，需要%s，缺少%s", deployInsNum, (deployInsNum - agentHostList.size())));
        }
        //如果不需要获取资源，直接返回
        if (!getResource) {
            return null;
        }

        //2) 建议把磁盘、内存、cpu也纳入考虑，此处不考虑，直接选择头部的。
        agentHostList = agentHostList.subList(0, deployInsNum);
        //原则上部署几个实例就需要几台机器，但如果是redis-cluster架构，则在三台机器的基础上，构建6个实例实现三主三从
        if (dbDeployInfo.getDbType().equalsIgnoreCase(DbConstants.REDIS)
                && dbDeployInfo.getArchType().equalsIgnoreCase(DbConstants.ARCH_TYPE_CLUSTER)) {
            agentHostList.addAll(new ArrayList<>(agentHostList));
        }
        return agentHostList;
    }

    @Override
    public void saveApply(ClusterApplyVo applyVo) {
        DbDeployInfo dbDeployInfo = new DbDeployInfo();
        BeanUtil.copy(applyVo, dbDeployInfo);
        dbDeployInfo.setCreator(JSON.toJSONString(applyVo.getCreator()));
        dbDeployInfoService.save(dbDeployInfo);
    }

    /**
     * 各DB根据需要@Override此调度算法，此处为最基本的调度
     */
    @Override
    public List<DbDeployInstance> getDbDeployInstances(DbDeployInfo dbDeployInfo) {
        //如果已经选定好机器，则使用选定好的机器
        List<DbDeployInstance> deployInstances = deployInstanceService.list(Condition.<DbDeployInstance>create().eq(DbDeployInstance.DEPLOY_ID, dbDeployInfo.getId()));
        if (CommonUtil.notNullOrEmpty(deployInstances)) {
            return deployInstances;
        }
        //检查并获取部署机器资源
        List<AssetsAgentHost> avaliavleResources = checkAndGetResource(dbDeployInfo, true);
        //选择部署机器的端口。
        int defaultBeginPort = getDbDefaultBeginPort(dbDeployInfo.getDbType());
        //部署的实例是否需要相同端口，MySQL端口需一致。
        int samePort = 0;
        if (dbNeedSamePort(dbDeployInfo)) {
            samePort = getSameAvailablePort(avaliavleResources, defaultBeginPort);
        }
        //记录已经选择的实例端口。
        Set<String> tmpSelectedPortSet = new HashSet<>();
        for (int i = 0; i < avaliavleResources.size(); i++) {
            AssetsAgentHost agentHost = avaliavleResources.get(i);
            //如果需要相同端口，则选择相同的端口。否则选用一个可用的端口。
            int choosePort = samePort > 0 ? samePort : getAvailablePort(agentHost, defaultBeginPort, tmpSelectedPortSet);
            //保存部署实例
            DbDeployInstance deployInstance = getDbDeployInstanceByDeployInfo(dbDeployInfo, agentHost.getHost(), choosePort);
            deployInstances.add(deployInstance);
            //锁定机器
            agentHost.setIsLock(AssetsAgentHost.LOCK_STATU);
            agentHostService.saveOrUpdate(agentHost);
        }
        return deployInstances;
    }

    private DbDeployInstance getDbDeployInstanceByDeployInfo(DbDeployInfo dbDeployInfo, String host, int choosePort) {
        DbDeployInstance deployInstance = new DbDeployInstance();
        deployInstance.setHost(host);
        deployInstance.setPort(String.valueOf(choosePort));
        deployInstance.setDeployId(dbDeployInfo.getId());
        deployInstance.setDbType(dbDeployInfo.getDbType());
        deployInstance.setIdcCode(dbDeployInfo.getIdcCode());
        deployInstance.setIdcName(dbDeployInfo.getIdcCode());
        deployInstance.setEnv(dbDeployInfo.getEnv());
        deployInstance.setVersion(dbDeployInfo.getVersion());
        deployInstance.setDeployStatus(DeployConstants.UN_DEPLOY);
        deployInstance.setCreateTime(new Date());
        deployInstance.setUpdateTime(deployInstance.getCreateTime());
        deployInstanceService.save(deployInstance);
        return deployInstance;
    }

    private int getAvailablePort(AssetsAgentHost avaliavleResource, int defaultBeginPort, Set<String> tmpSelectedPortSet) {
        int portInterval = 1;
        String host = avaliavleResource.getHost();
        //资源中只要有一个端口被使用，则本次选择的端口不可用，尝试下一个端口。
        for (int i = 0; i < 300; i++) {
            //资源中端口被使用或选用，则尝试下一个端口。
            Integer selectedPort = defaultBeginPort + (i * portInterval);
            if (assetsInstanceService.count(Condition.<AssetsInstance>create().eq(AssetsInstance.HOST, host).eq(AssetsInstance.PORT, selectedPort)) > 0
                    || tmpSelectedPortSet.contains(avaliavleResource.getHost() + ":" + selectedPort)) {
                continue;
            }
            boolean portUsed = IPUtil.isPortUsed(host, selectedPort);
            if (!portUsed) {
                tmpSelectedPortSet.add(host + ":" + selectedPort.toString());
                return selectedPort;
            }
        }
        throw new RuntimeException(String.format("机器[%s]无法找到有效端口部署", avaliavleResource.getHost()));
    }

    private boolean dbNeedSamePort(DbDeployInfo dbDeployInfo) {
        if (dbDeployInfo.getDbType().equalsIgnoreCase(DbConstants.MYSQL)) {
            return true;
        }

        return false;
    }

    private int getDeployInsNum(DbDeployInfo dbDeployInfo) {
        String archType = dbDeployInfo.getArchType();
        if (dbDeployInfo.getDbType().equalsIgnoreCase(DbConstants.MYSQL)) {
            return archType.equalsIgnoreCase(DbConstants.ARCH_TYPE_STANDARD) ? 1 : 2;
        }
        if (dbDeployInfo.getDbType().equalsIgnoreCase(DbConstants.REDIS)) {
            return archType.equalsIgnoreCase(DbConstants.ARCH_TYPE_STANDARD) ? 1 : 3;
        }
        throw new RuntimeException(String.format("[%s]类型架构[%s]未指定部署实例个数", dbDeployInfo.getDbType(), dbDeployInfo.getArchType()));
    }

    /**
     * 从beginPort端口开始，在可用资源中找到相同可用端口。
     *
     * @return
     */
    private Integer getSameAvailablePort(List<AssetsAgentHost> avaliableResource, int beginPort) {
        //端口查询间隔：10000,10001,10002...10300。
        int portInterval = 1;
        //共检查300个端口
        int portCheckNum = 300;
        portLoop:
        for (int i = 0; i < portCheckNum; i++) {
            Integer selectedPort = beginPort + (i * portInterval);
            //资源中只要有一个端口被使用，则本次选择的端口不可用，尝试下一个端口。
            for (AssetsAgentHost resourceInfo : avaliableResource) {
                String host = resourceInfo.getHost();
                //端口被使用情况1：实例数据已经记录
                if (assetsInstanceService.getOne(Condition.<AssetsInstance>create().eq(AssetsInstance.HOST, host)
                        .eq(AssetsInstance.PORT, selectedPort)) != null) {
                    continue portLoop;
                }
                //端口被使用情况2：机器端口被占用
                boolean portUsed = IPUtil.isPortUsed(host, selectedPort);
                if (portUsed) {
                    continue portLoop;
                }
            }
            return selectedPort;
        }
        throw new RuntimeException(String.format("机器[%s]无法找到相同的可用端口", avaliableResource.stream().map(e -> e.getHost()).collect(Collectors.joining(","))));
    }


    private int getDbDefaultBeginPort(String dbType) {
        if (dbType.equalsIgnoreCase(DbConstants.MYSQL)) {
            return Integer.valueOf(sysDictService.getUniValueByGroupAndKey("db_deploy", "mysql_default_port", 11000));
        }
        if (dbType.equalsIgnoreCase(DbConstants.REDIS)) {
            return Integer.valueOf(sysDictService.getUniValueByGroupAndKey("db_deploy", "redis_default_port", 12000));
        }
        throw new RuntimeException("未指定该数据库实例默认的起始端口！");
    }

    @Override
    @Transactional
    public void deploySuccess(DbDeployInfo dbDeployInfo, WorkOrderOperateVo operateVo) {
        //1）新增集群数据
        AssetsCluster cluster = new AssetsCluster();
        cluster.setDbType(dbDeployInfo.getDbType());
        cluster.setClusterName(dbDeployInfo.getClusterName());
        cluster.setClusterType(DbConstants.PHY);
        cluster.setIdcCode(dbDeployInfo.getIdcCode());
        cluster.setIdcName(dbDeployInfo.getIdcCode());
        cluster.setEnv(dbDeployInfo.getEnv());
        cluster.setArchType(dbDeployInfo.getArchType());
        cluster.setCreator(dbDeployInfo.getCreator());
        cluster.setCapacity(getDbCapacity(dbDeployInfo));
        cluster.setPackageName(dbDeployInfo.getPackageName());
        cluster.setVersion(dbDeployInfo.getVersion());
        cluster.setCreateTime(new Date());
        assetsClusterService.save(cluster);
        //2）新增实例数据、修改机器资源数
        List<DbDeployInstance> deployInstances = deployInstanceService.list(Condition.<DbDeployInstance>create().eq(DbDeployInstance.DEPLOY_ID, dbDeployInfo.getId()));
        for (DbDeployInstance deployInstance : deployInstances) {
            deployInstance.setId(null);
            AssetsInstance assetsInstance = new AssetsInstance();
            BeanUtil.copy(deployInstance, assetsInstance);
            assetsInstance.setClusterId(cluster.getId());
            assetsInstanceService.save(assetsInstance);
            AssetsAgentHost agentHost = agentHostService.getOne(Condition.<AssetsAgentHost>create()
                    .eq(AssetsAgentHost.HOST, assetsInstance.getHost())
                    .eq(AssetsAgentHost.DBTYPE, deployInstance.getDbType())
            );
            agentHost.setExistsInsNum(agentHost.getExistsInsNum() + 1);
            int supportInsNum = agentHost.getSupportInsNum() - 1;
            if (dbDeployInfo.getPackageName().equalsIgnoreCase(DbConstants.PACKAGE_X)) {
                supportInsNum = 0;
            }
            agentHost.setSupportInsNum(supportInsNum);
            agentHost.setIsLock(AssetsAgentHost.UNLOCK_STATU);
            agentHostService.saveOrUpdate(agentHost);
        }
        //3）更新工单执行状态
        UpdateWrapper updateWrapper = new UpdateWrapper();
        updateWrapper.eq(WorkOrder.ID, operateVo.getId());
        updateWrapper.set(WorkOrder.CLUSTER_ID, cluster.getId());
        workOrderService.update(new WorkOrder(), updateWrapper);
        workOrderService.executeSuccess(operateVo);
    }

    private String getDbCapacity(DbDeployInfo dbDeployInfo) {
        QueryWrapper wrapper = new QueryWrapper();
        wrapper.eq(AssetsDbPackage.DB_TYPE, dbDeployInfo.getDbType());
        wrapper.eq(AssetsDbPackage.PACKAGE_NAME, dbDeployInfo.getPackageName());
        AssetsDbPackage dbPackage = dbPackageService.getOne(wrapper);
        return dbPackage.getPackageCapacity().contains("-") ? dbPackage.getPackageCapacity().split("-")[1] : dbPackage.getPackageCapacity();
    }

    @Override
    public void deployFail(DbDeployInfo dbDeployInfo, String errMsg) {
        workOrderService.executeFail(dbDeployInfo.getWorkOrderId(), errMsg);
    }
}
